﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;
using System.Diagnostics;
using System.IO;
//using System.Windows.Controls.Theming;

namespace Nav
{
    public class MenuClickedEventArgs: EventArgs 
    {
        public string Name
        {
            get;
            set;
        }
        public string Data
        {
            get;
            set;
        }
    }

    [TemplatePart(Name = NavigationMenu.TP_CANVAS_PART, Type = typeof(Canvas))]
    [TemplatePart(Name = NavigationMenu.TP_HOME_BUTTON_PART, Type = typeof(Button))]
    [TemplatePart(Name = NavigationMenu.TP_PREV_BUTTON_PART, Type = typeof(Button))]
    [TemplatePart(Name = NavigationMenu.TP_NEXT_BUTTON_PART, Type = typeof(Button))]
    [TemplatePart(Name = NavigationMenu.TP_CURRENTMENU_PART, Type = typeof(TextBlock))]
    public class NavigationMenu : Control
    {
        #region Constants
        private const string                            ERROR_TITLE = "Error";
        private const int                               ANIMATION_DURATION = 500;
        
        private const string                            TP_CANVAS_PART = "itemsCanvas";
        private const string                            TP_HOME_BUTTON_PART = "home";
        private const string                            TP_PREV_BUTTON_PART = "prevButton";
        private const string                            TP_NEXT_BUTTON_PART = "nextButton";
        private const string                            TP_CURRENTMENU_PART = "currentMenu";

        #endregion

        #region Private Fields

        // This is canvas where navigation menu bands (m_active and m_inactive) are located
        private Canvas                                  m_itemCanvas;

        // Navigation bands with menu items. Active is visible on the screen and 
        // inactive is not visible. When user clicks menu or prev/next buttons we 
        // animate (slide) these bands so that active becomes inactive and vs.
        private NavigationMenuBand                      m_active;
        private NavigationMenuBand                      m_inactive;

        // We can only initialize bands when we know size of parent canvas.
        // This happens when the canvas is resized
        private bool                                    m_bandsInitialized;

        private Button                                  m_prevButton;
        private Button                                  m_nextButton;
        private Button                                  m_home;
        private TextBlock                               m_currentMenu;

        // If animation is not yet finished this value is not null, so if the user 
        // wants to animate while current animation is not finished, we finish it 
        // first
        private Storyboard                              m_currAnimation;

        // true if we in a process of loading data from server
        private bool                                    m_loading;

        // root node of menu items
        private Node                                    m_root;

        // stack of previous and next menu items
        private List<Node>                              m_path = new List<Node>();

        // Current position in menu item stack (which is in m_path)
        private int                                     m_currentPathId = 0;

        private string                                  m_theme = "MenuDefaultTheme.xaml";

        #endregion

        public EventHandler<MenuClickedEventArgs>       MenuClick;
        
        
        public NavigationMenu()
        {
            this.DefaultStyleKey = typeof(NavigationMenu);
            this.Loaded += (s, e) => BeginDataDownload();
        }

        #region Dependency Properties

        public static readonly DependencyProperty OrientationProperty = 
            DependencyProperty.Register("Orientation", typeof(Orientation),
            typeof(NavigationMenu), new PropertyMetadata(Orientation.Horizontal));

        public Orientation Orientation
        {
            get 
            { 
                return (Orientation) GetValue(OrientationProperty); 
            }
            set 
            { 
                SetValue(OrientationProperty, value); 
            }
        }

        public static readonly DependencyProperty DataUrlProperty = 
            DependencyProperty.Register("DataUrl", typeof(string),
            typeof(NavigationMenu), new PropertyMetadata(String.Empty));

        public string DataUrl
        {
            get 
            { 
                return (string) GetValue(DataUrlProperty); 
            }
            set 
            { 
                SetValue(DataUrlProperty, value); 
            }
        }

        #endregion

        public override void OnApplyTemplate()
        {
            base.OnApplyTemplate();

            m_itemCanvas = (Canvas) GetTemplateChild(TP_CANVAS_PART);
            Debug.Assert(m_itemCanvas != null);

            if (m_itemCanvas != null)
            {
                m_itemCanvas.SizeChanged += OnCanvasSizeChanged;
            }

            m_prevButton = (Button) GetTemplateChild(TP_PREV_BUTTON_PART);
            Debug.Assert(m_prevButton != null);

            m_nextButton = (Button) GetTemplateChild(TP_NEXT_BUTTON_PART);
            Debug.Assert(m_nextButton != null);

            m_home = (Button) GetTemplateChild(TP_HOME_BUTTON_PART);
            Debug.Assert(m_home != null);

            m_currentMenu = (TextBlock) GetTemplateChild(TP_CURRENTMENU_PART);
            Debug.Assert(m_currentMenu != null);

            m_prevButton.Click += OnPrevButton;
            m_nextButton.Click += OnNextButton;
            m_home.Click += OnHome;

            m_active = null;
            m_inactive = null;
            m_bandsInitialized = false;

            UpdateButtonStates();
        }

        void OnHome(object sender, RoutedEventArgs e)
        {
            if (m_currentPathId > 0)
            {
                m_currentPathId = 0;
                m_inactive.ItemList = m_path[0].Nodes;
                Animate(false);
            }
        }

        void OnPrevButton(object sender, RoutedEventArgs e)
        {
            if (m_currentPathId > 0)
            {
                m_inactive.ItemList = m_path[--m_currentPathId].Nodes;
                Animate(false);
            }
        }

        void OnNextButton(object sender, RoutedEventArgs e)
        {
            if (m_currentPathId + 1 < m_path.Count())
            {
                m_inactive.ItemList = m_path[++m_currentPathId].Nodes;
                Animate(true);
            }
        }

        /// 
        /// <summary>
        /// This switches (slides) active and inactive navigation menu band. </summary>
        /// 
        private void Animate(bool forward)
        {
            Storyboard                                  sb;
            NavigationMenuBand                          tmp;
            double                                      activeFrom;
            double                                      activeTo;
            double                                      inactiveFrom;
            double                                      inactiveTo;
            string                                      property;
            int                                         c;


            // if we are already in process of transition/animation, then we need to finish 
            // it first
            if (m_currAnimation != null)
            {
                m_currAnimation.SkipToFill();
            }

            inactiveTo = 0;
            activeFrom = 0;

            c = forward ? -1 : 1;

            property = (Orientation == Orientation.Horizontal ? "(Canvas.Left)" : "(Canvas.Top)");

            if (Orientation == Orientation.Horizontal)
            {
                m_inactive.SetValue(Canvas.LeftProperty, -c * m_inactive.ActualWidth);
                m_inactive.SetValue(Canvas.TopProperty, 0.0);
            }
            else
            {
                m_inactive.SetValue(Canvas.LeftProperty, 0.0);
                m_inactive.SetValue(Canvas.TopProperty, -c * m_inactive.ActualHeight);
            }

            activeTo = (Orientation == Orientation.Horizontal ? 
                c * m_active.ActualWidth : 
                c * m_active.ActualHeight);

            inactiveFrom = (Orientation == Orientation.Horizontal ? 
                -c * m_inactive.ActualWidth : 
                -c * m_inactive.ActualHeight);

            sb = new Storyboard();

            sb.Children.Add(CreateDoubleAnimation(m_inactive, property, inactiveFrom, inactiveTo));
            sb.Children.Add(CreateDoubleAnimation(m_inactive, "(UIElement.Opacity)", 0, 1));
            sb.Children.Add(CreateDoubleAnimation(m_active, property, activeFrom, activeTo));
            sb.Children.Add(CreateDoubleAnimation(m_active, "(UIElement.Opacity)", 1, 0));

            sb.Begin();

            // reset current animation when it finished
            sb.Completed += (s, e) => m_currAnimation = null;

            m_currAnimation = sb;

            UpdateButtonStates();

            tmp = m_inactive;
            m_inactive = m_active;
            m_active = tmp;
        }

        /// 
        /// <summary>
        /// Helper to create double animation</summary>
        /// 
        private DoubleAnimation CreateDoubleAnimation(DependencyObject element, string property, 
            double from, double to)
        {
            DoubleAnimation                                 da;
            int                                             time;

            da = new DoubleAnimation();
            da.To = to;
            da.From = from;

            time = ANIMATION_DURATION;
            if (Orientation == Orientation.Horizontal)
            {
                time /= 2;
            }

            da.Duration = new Duration(new TimeSpan(0, 0, 0, 0, time));

            Storyboard.SetTargetProperty(da, new PropertyPath(property));
            Storyboard.SetTarget(da, element);
            return da;
        }

        /// 
        /// <summary>
        /// Here as we know canvas size we can create active and inactive menu 
        /// bands</summary>
        /// 
        void OnCanvasSizeChanged(object sender, SizeChangedEventArgs e)
        {
            m_itemCanvas.Clip = new RectangleGeometry()
            {
                Rect = new Rect(0, 0, m_itemCanvas.ActualWidth, m_itemCanvas.ActualHeight)
            };

            if (!m_bandsInitialized)
            {
                m_active = new NavigationMenuBand();
                m_inactive = new NavigationMenuBand();
                
                m_itemCanvas.Children.Add(m_active);
                m_itemCanvas.Children.Add(m_inactive);

                m_active.SetValue(Canvas.TopProperty, 0.0);
                m_active.SetValue(Canvas.LeftProperty, 0.0);

                m_inactive.SetValue(Canvas.TopProperty, 0.0);
                m_inactive.SetValue(Canvas.LeftProperty, m_itemCanvas.ActualWidth);
                
                m_bandsInitialized = true;

                if (m_path.Count() != 0)
                {
                    m_active.ItemList = m_path[m_currentPathId].Nodes;
                }
            }

            m_active.Width = m_itemCanvas.ActualWidth;
            m_active.Height = m_itemCanvas.ActualHeight;            

            m_inactive.Width = m_itemCanvas.ActualWidth;
            m_inactive.Height = m_itemCanvas.ActualHeight;
        }


        /// 
        /// <summary>
        /// Depending on where we are in m_path we disable/enable prev/next buttons</summary>
        /// 
        private void UpdateButtonStates()
        {
            m_nextButton.IsEnabled = m_path.Count() > m_currentPathId + 1;
            m_home.IsEnabled = m_currentPathId >= 3;

            m_currentMenu.Text =  m_path.Count() > 0 ? m_path[m_currentPathId].Name : 
                String.Empty;

            m_prevButton.Content = m_currentPathId > 0 ? "<< " + m_path[m_currentPathId - 1].Name :
                String.Empty;

            m_prevButton.Visibility = m_currentPathId > 0 ? Visibility.Visible : 
                Visibility.Collapsed;
        }

        /// 
        /// <summary>
        /// This method invoked from NavigationMenuItem when the user clicks menu. This method 
        /// assigned to each Node in OnDataReadCompleted</summary>
        /// 
        private void Action(Node n)
        {
            //
            // remove all forward menus
            //
            m_inactive.ItemList = n.Nodes;
            if (m_currentPathId + 1 < m_path.Count())
            {
                m_path.RemoveRange(m_currentPathId + 1, m_path.Count() - m_currentPathId - 1);
            }
            UpdateButtonStates();

            //
            // Now do the action (if the node has child nodes then we navigate to next 
            // menu level otherwise do action assigned to the node
            //
            if (n.Nodes.Count() > 0)
            {
                m_path.Add(n);
                m_currentPathId++;

                Animate(true);
            }
            else
            {
                if (MenuClick != null)
                {
                    MenuClick(this, new MenuClickedEventArgs() 
                    {
                        Name = n.Name,
                        Data = n.Data
                    });
                }
            }
        }

        public string Theme
        {
            get
            {
                return m_theme;
            }

            set
            {
                Uri                                     uri;
                
                uri = new Uri(@"Nav;component/themes/" + value, UriKind.Relative);

                //ImplicitStyleManager.SetResourceDictionaryUri(this, uri);
                //ImplicitStyleManager.SetApplyMode(this, ImplicitStylesApplyMode.Auto);
                //ImplicitStyleManager.Apply(this);
            }
        }

        #region Data Reading


        /// 
        /// <summary>
        /// The user clicked start button, so we go to the server to get content</summary>
        /// 
        void BeginDataDownload()
        {
            WebClient                               client;

            if (DataUrl.Length > 0 && !m_loading)
            {
                m_loading = true;

                client = new WebClient();
                client.OpenReadCompleted += OnDataReadCompleted;
                client.OpenReadAsync(new Uri(DataUrl, UriKind.RelativeOrAbsolute));
            }
        }

        /// 
        /// <summary>
        /// This called when data (or error) from server available, so we can 
        /// populate menu and reset control state</summary>
        /// 
        void OnDataReadCompleted(object sender, OpenReadCompletedEventArgs e)
        {
            Action<Node>                                assignCommand = null;

            //
            // This action recursively goes through all nodes in the tree and assign 
            // this.Action callback to all nodes. This Command event will be called
            // from NavigationMenuItem when user clicks it.
            //
            assignCommand = (n) => 
            {
                n.Command += this.Action; 
                n.Nodes.ForEach(assignCommand);
            };
    
            if (e.Error != null)
            {
                MessageBox.Show(e.Error.Message, ERROR_TITLE, MessageBoxButton.OK);
            }
            else 
            {
                // clear current stack of menu items and reset menu tree
                m_path.Clear();
                m_root = Node.Deserialize(e.Result);
                assignCommand(m_root);

                // add initial menu (from root node)
                m_path.Add(m_root); 
                m_currentPathId = 0;
                UpdateButtonStates();


                // if we have at least one level of menu then slow it
                if (m_path.Count() > m_currentPathId)
                {
                    m_active.ItemList = m_path[m_currentPathId].Nodes;
                }
                else
                {
                    m_active.ItemList = null;
                }

            }

            // enable back start button and reset loading flag
            m_loading = false;
        }

        #endregion
    }
}
